package com.zoe.onelink.doc.registry;

import cn.hutool.core.net.NetUtil;
import com.github.xiaoymin.knife4j.core.util.StrUtil;
import com.github.xiaoymin.knife4j.spring.extension.OpenApiExtensionResolver;
import com.zoe.onelink.common.constant.OnelinkConstant;
import com.zoe.onelink.doc.property.OnelinkDocProperties;
import lombok.extern.slf4j.Slf4j;
import org.springframework.beans.BeansException;
import org.springframework.beans.factory.config.ConfigurableListableBeanFactory;
import org.springframework.beans.factory.support.AbstractBeanDefinition;
import org.springframework.beans.factory.support.BeanDefinitionBuilder;
import org.springframework.beans.factory.support.BeanDefinitionRegistry;
import org.springframework.beans.factory.support.BeanDefinitionRegistryPostProcessor;
import org.springframework.boot.context.properties.bind.Binder;
import org.springframework.context.EnvironmentAware;
import org.springframework.core.env.Environment;
import org.springframework.util.AntPathMatcher;
import springfox.documentation.RequestHandler;
import springfox.documentation.builders.ApiInfoBuilder;
import springfox.documentation.builders.ParameterBuilder;
import springfox.documentation.builders.PathSelectors;
import springfox.documentation.schema.ModelRef;
import springfox.documentation.service.ApiInfo;
import springfox.documentation.service.Parameter;
import springfox.documentation.spi.DocumentationType;
import springfox.documentation.spring.web.plugins.Docket;

import java.util.ArrayList;
import java.util.Arrays;
import java.util.List;
import java.util.Map;
import java.util.function.Predicate;

/**
 * <p>标题: </p>
 * <p>描述: </p>
 * <p>版权: Copyright (c) 2022</p>
 * <p>公司: 智业软件股份有限公司</p>
 *
 * @version: 1.0
 * @author: ruansheng
 * @date: 2022-03-30
 */
@Slf4j
public class OnelinkKnife4jDocRegistry implements BeanDefinitionRegistryPostProcessor, EnvironmentAware {

    public static final String BEAN_PREFIX = "ONELINK_DOCKET_";
    private final AntPathMatcher antPathMatcher = new AntPathMatcher();
    /*引入Knife4j提供的扩展类*/
    private final OpenApiExtensionResolver openApiExtensionResolver;
    private Environment environment;

    public OnelinkKnife4jDocRegistry(OpenApiExtensionResolver openApiExtensionResolver) {
        this.openApiExtensionResolver = openApiExtensionResolver;
    }

    @Override
    public void postProcessBeanDefinitionRegistry(BeanDefinitionRegistry registry) throws BeansException {

        OnelinkDocProperties onelinkDocProperties = Binder.get(this.environment)
                .bindOrCreate(OnelinkDocProperties.PROPERTY_PREFIX, OnelinkDocProperties.class);

        if (!onelinkDocProperties.isEnabled()) {
            log.info("[接口文档] 组件未开启. 如需开启,请设置 [onelink.doc.enabled=true]");
            return;
        }

        Map<String, String> apiGroup = onelinkDocProperties.getApiGroup();

        // 如果没有配置则创建一个默认的分组
        if (apiGroup.isEmpty()) {
            String groupName = "default";
            Docket defaultDocket = this.createDocket(groupName, "com.zoe.**");
            this.registerDocket(registry, BEAN_PREFIX + groupName, defaultDocket);
            return;
        }

        // 根据配置创建对应的分组
        for (Map.Entry<String, String> e : apiGroup.entrySet()) {
            String groupName = e.getKey();
            // 移除空格
            String basePackagePatterns = e.getValue().replace(" ", "");
            if (StrUtil.isBlank(basePackagePatterns)) {
                log.warn("[接口文档] 分组[{}]包扫描路径为空", groupName);
            }
            Docket docket = this.createDocket(groupName, basePackagePatterns);
            log.info("[接口文档] 创建接口文档分组: {} , 扫描包路径: {}", groupName, basePackagePatterns);
            this.registerDocket(registry, BEAN_PREFIX + groupName, docket);
        }

    }

    private void registerDocket(BeanDefinitionRegistry registry, String beanName, Docket docket) {
        AbstractBeanDefinition beanDefinition = BeanDefinitionBuilder.genericBeanDefinition(Docket.class, () -> docket)
                .getBeanDefinition();
        registry.registerBeanDefinition(beanName, beanDefinition);
    }

    @Override
    public void postProcessBeanFactory(ConfigurableListableBeanFactory beanFactory) throws BeansException {

    }

    @Override
    public void setEnvironment(Environment environment) {
        this.environment = environment;
    }

    /**
     * 创建文档分组
     */
    public Docket createDocket(String groupName, String basePackages) {
        List<Parameter> parameterList = new ArrayList<>();
        parameterList.add(this.createDefaultHeader(OnelinkConstant.AUTH_TOKEN_HEADER, "system"));
        try {
            parameterList.add(this.createDefaultHeader("clientIp", NetUtil.getLocalhostStr()));
        } catch (Exception e) {
            log.warn("[OnelinkDoc] Get local ip failure: {}", e.getMessage());
        }
        return new Docket(DocumentationType.SWAGGER_2)
                .enable(true)
                .groupName(groupName)
                .apiInfo(apiInfo())
                .globalOperationParameters(parameterList)
                .select()
                .apis(this.pathSelector(basePackages))
                .paths(PathSelectors.any())
                .build()
                .extensions(openApiExtensionResolver.buildExtensions(groupName));
    }

    /**
     * 创建默认请求头
     */
    private Parameter createDefaultHeader(String headerName, String defaultValue) {
        return new ParameterBuilder()
                .name(headerName)
                .defaultValue(defaultValue)
                .description(OnelinkConstant.AUTH_TOKEN_HEADER + " header")
                .modelRef(new ModelRef("string"))
                .parameterType("header")
                .required(false)
                .build();
    }

    /**
     * 路径选择器
     */
    private Predicate<RequestHandler> pathSelector(String basePackages) {
        return requestHandler -> {
            if (StrUtil.isBlank(basePackages)) {
                return false;
            }
            Class<?> clazz = requestHandler.declaringClass();
            if (clazz == null) {
                return false;
            }
            String classPackage = clazz.getPackageName();
            return Arrays.stream(basePackages.split(","))
                    .anyMatch(basePackagePattern ->
                            classPackage.startsWith(basePackagePattern)
                                    || this.antPathMatcher.match(basePackagePattern, classPackage));
        };
    }

    private ApiInfo apiInfo() {
        return new ApiInfoBuilder()
                .description("智业软件股份有限公司")
                .termsOfServiceUrl("http://www.zysoft.com.cn//")
                .version("1.0")
                .build();
    }
}
